<?php

defined('__MAPEXEC__') or die('No direct access allowed');

/**
 * Manages connection to a database, and the insertion, retrieval and removal
 * of geographic coordinates.
 */
class Maps_Geocode_DB {
  
  private $autoCommit = true;
  private $dbconf = array();
  private $dbh = null;
  private $dbpass = '';
  private $dbuser = '';
  private $dsn = '';
  private $connectionOpen = false;
  private static $me = null;
  private $pendingCoords = array();
  
  const SQL_INSERT = 'REPLACE INTO GEO_COORDINATES (address, lat, lng) VALUES (?, ?, ?)';
  const SQL_SELECT = 'SELECT * FROM GEO_COORDINATES WHERE address = ?';
  
  /**
   * Class constructor.
   * @param   string  Data source name
   * @param   string  Data base user name
   * @param   string  Data base password
   * @param   array   Array of PDO configs
   * @return  void
   */
  private function __construct($dsn, $dbuser, $dbpass, $dbconf=array()) {
    $this->setDSN($dsn);
    $this->setDBUser($dbuser);
    $this->setDBPass($dbpass);
    $this->setDBConfig($dbconf);
  }
  
  /**
   * Class destructor.
   * @param   void
   * @return  void
   */
  public function __destroy() {
    $this->disconnect();
  }
  
  /**
   * Disable the auto commit feature.
   * @param   void
   * @return  void
   */
  public function autoCommitDisabled() {
    $this->autoCommit = false;
  }
  
  /**
   * Enable the auto commit feature.
   * @param   void
   * @return  void
   */
  public function autoCommitEnabled() {
    $this->autoCommit = true;
  }
  
  /**
   * Commits the pending coordinates to the database.
   * @param   void
   * @return  void
   */
  public function commit() {
    
    if ( empty($this->pendingCoords) || !$this->connect() ) {
      return;
    }
    
    $stmt = $this->dbh->prepare( Maps_Geocode_DB::SQL_INSERT );
    $stmt->bindParam(1, $address);
    $stmt->bindParam(2, $lat);
    $stmt->bindParam(3, $lng);
    
    try {
      $this->dbh->beginTransaction();
      
      foreach ($this->pendingCoords as $address => $coord) {
        $address = $this->encode($address);
        $lat = $coord['lat'];
        $lng = $coord['lng'];
        $stmt->execute();
      }
      
      $this->dbh->commit();
      $this->flushPendingCoords();
    } catch (Exception $e) {
      $this->dbh->rollBack();
      die( $e->getMessage() );
    }
    
  }
  
  /**
   * Connects to the database.
   * @param   void
   * @return  boolean   True if connection is open
   */
  private function connect() {
    $dsn = $this->getDSN();
    $user = $this->getDBUser();
    $pass = $this->getDBPass();
    $dbconf = $this->getDBConfig();
    
    if ( !$this->isConnected() && !empty($dsn) && !empty($user) && !empty($pass) ) {
      try {
        $this->dbh = new PDO($dsn, $user, $pass, $dbconf);
      } catch (Exception $e) {
        die( $e->getMessage() );
      }
      $this->connectionOpen = true;
    }
    
    return $this->connectionOpen;
  }
  
  /**
   * Disconnects from the database.
   * @param   void
   * @return  boolean   True if connection is closed
   */
  private function disconnect() {
    
    if ( !empty($this->pendingCoords) && $this->autoCommit ) {
      $this->commit();
    }
    
    $this->flushPendingCoords();
    
    if ( $this->isConnected() ) {
      $this->dbh = null;
      $this->connectionOpen = false;
    }
    
    return !$this->connectionOpen;
  }
  
  /**
   * Gets coordinates for an address from the database. First searches
   * pending coordinates, then the database.
   * @param   string  Fully qualified street address
   * @return  Maps_LatLng   Latitude/longitude object or false when not found.
   */
  public function fetch($address) {
    $coords = array();
    $result = null;
    $stmt = null;
    
    if ( isset($this->pendingCoords[$address]) ) {
      $coords = $this->pendingCoords[$address];
    } elseif ( $this->connect() ) {
      $stmt = $this->dbh->prepare( Maps_Geocode_DB::SQL_SELECT );
      $key = $this->encode($address);
      
      if ( $stmt->execute(array($key)) ) {
        $result = $stmt->fetch();
        if ( !empty($result['lat']) && !empty($result['lng']) ) {
          $coords['lat'] = $result['lat'];
          $coords['lng'] = $result['lng'];
        }
      }
    }
    return empty($result) ? false : new Maps_LatLng($coords['lat'], $coords['lng']);
  }
  
  /**
   * Removing any coordinates pending insertion into the database.
   * @param   void
   * @return  void
   */
  private function flushPendingCoords() {
    $this->pendingCoords = array();
  }
  
  /**
   * Returns an encoded string.
   * @param   string  String to be encoded
   * @return  string  Encoded string
   */
  private function encode($str) {
    return md5($str);
  }
  
  /**
   * Get the data base config values.
   * @param   void
   * @return  array   Array of database configs
   */
  private function getDBConfig() {
    return $this->dbconf;
  }
  
  /**
   * Get the database password.
   * @param   void
   * @return  string  Data base password.
   */
  private function getDBPass() {
    return $this->dbpass;
  }
  
  /**
   * Gets the data base user name.
   * @param   void
   * @return  string  Data base user name.
   */
  private function getDBUser() {
    return $this->dbuser;
  }
  
  /**
   * Gets the data source name for this database connection.
   * @param   void
   * @return  string  Data source name
   */
  private function getDSN() {
    return $this->dsn;
  }
  
  /**
   * Initializes the single instance of this class.
   * @param   string  Data source name
   * @param   string  Data base user name
   * @param   string  Data base password
   * @param   array   Array of PDO configs
   * @return  object  The only instance of this class
   */
  public static function initialize($dsn, $dbuser, $dbpass, $dbconf=array()) {
    if ( isset(self::$me) ) {
      return;
    }
    self::$me = new self($dsn, $dbuser, $dbpass, $dbconf);
  }
  
  /**
   * Inserts coordinates into a "pending coordinates" array. The commit() method
   * must be called before the data is saved to the database.
   * @param   string  Fully qualified street address
   * @param   Maps_LatLng   Latitude/longitude object
   * @return  void
   */
  public function insert($address, Maps_LatLng $coords) {
    $this->pendingCoords[$address] = $coords->toArray();
  }
  
  /**
   * Static function to get the online instance of this class available.
   * @param   void
   * @return  Maps_Geocode_DB   Geocode data base object
   */
  public static function instance() {
    if ( !isset(self::$me) ) {
      die( new Exception('Error: Maps_Geocode_DB::initialize() must be called prior to Maps_Geocode_DB::instance().') );
    }
    return self::$me;
  }
  
  /**
   * Detects if the database connection exists.
   * @param   void
   * @return  boolean   True if connected.
   */
  private function isConnected() {
    return $this->connectionOpen;
  }
  
  /**
   * Sets the data base config options.
   * @param   array   Array of PDO configs
   * @return  void
   */
  private function setDBConfig($dbconf) {
    if ( is_array($dbconf) && !empty($dbconf) ) {
      $this->dbconf = $dbconf;
    }
  }
  
  /**
   * Sets the data base password.
   * @param   string  Data base password
   * @return  void
   */
  private function setDBPass($pass) {
    $this->dbpass = $pass;
  }
  
  /**
   * Sets the data base user name.
   * @param   string  Data base user name
   * @return  void
   */
  private function setDBUser($user) {
    $this->dbuser = $user;
  }
  
  /**
   * Sets the data source name.
   * @param   string  Data source name
   * @return  void
   */
  private function setDSN($dsn) {
    $this->dsn = $dsn;
  }
  
}

?>